dma-buf学习分享
作者简介
1.dma-buf简介
dma-buf是kernel提供的一个框架,它主要是为了解决不同设备驱动之间buf共享的问题。
2.示例说明
"Talk is cheap. Show me the code.",单纯的文字描述比较抽象,我们通过实验先来看下dma-buf是怎么使用的。
2.1 用户空间
/* test.c */
int main(void)
{
/* open dma-heap */
heap_fd = open("/dev/dma_heap/global_cma@68000000", O_RDWR);
data.len = 1024 * 1024; // 1M
data.fd = 0;
data.fd_flags = O_RDWR | O_CLOEXEC;
data.heap_flags = 0;
/* alloc buf */
ret = ioctl(heap_fd, DMA_HEAP_IOCTL_ALLOC, &data);
buf_fd = (int)data.fd;
p = mmap(NULL, 1024 * 1024, PROT_READ | PROT_WRITE, MAP_SHARED, buf_fd, 0);
/* driver A */
fda = open("/dev/driverA", O_RDWR);
infoa.fd = buf_fd;
infoa.buf = p;
infoa.size = 1024 * 1024; // 1M
strcpy((char *)infoa.buf, "driverA: userspace!");
ret = ioctl(fda, TEST_DRIVERA, &infoa);
printf("driverA buf = %s\n", infoa.buf);
/* driver B */
fdb = open("/dev/driverB", O_RDWR);
infob.fd = buf_fd;
infob.buf = p;
infob.size = 1024 * 1024; // 1M
ret = ioctl(fda, TEST_DRIVERB, &infob);
printf("driverB buf = %s\n", infob.buf);
return 0;
}
2.2 内核空间
/* test-driver.c */
static long test_ioctl(struct file *file, unsigned cmd, unsigned long arg)
{
int i;
struct buf_info info;
struct scatterlist *sg;
if (copy_from_user(&info, (void __user *)arg, sizeof(info)) != 0) {
printk("copy_from_user failed\n");
return -EFAULT;
}
printk("fd[%d], buf[0x%px], size[0x%x]\n", info.fd, info.buf, info.size);
switch(cmd) {
case TEST_DRIVERA:
/* for dma access */
test_dev.dma_buf = dma_buf_get(info.fd);
test_dev.attach = dma_buf_attach(test_dev.dma_buf, test_dev.miscdev.this_device);
test_dev.sg = dma_buf_map_attachment(test_dev.attach, DMA_TO_DEVICE);
for_each_sg(test_dev.sg->sgl, sg, test_dev.sg->nents, i) {
printk("<%s: %d>addr = 0x%08llx, len = 0x%x\n",
__FUNCTION__, __LINE__, sg->dma_address, sg->length);
}
dma_buf_unmap_attachment(test_dev.attach, test_dev.sg, DMA_TO_DEVICE);
dma_buf_detach(test_dev.dma_buf, test_dev.attach);
dma_buf_put(test_dev.dma_buf);
/* for cpu access */
dma_buf_vmap(test_dev.dma_buf, &test_dev.map);
printk("<%s: %d>addr = 0x%px, str = %s\n",
__FUNCTION__, __LINE__, test_dev.map.vaddr, (char *)test_dev.map.vaddr);
strcpy((char *)test_dev.map.vaddr, "driverA kernel space!");
dma_buf_vunmap(test_dev.dma_buf, &test_dev.map);
break;
case TEST_DRIVERB:
/* for dma access */
test_dev.dma_buf = dma_buf_get(info.fd);
test_dev.attach = dma_buf_attach(test_dev.dma_buf, test_dev.miscdev.this_device);
test_dev.sg = dma_buf_map_attachment(test_dev.attach, DMA_TO_DEVICE);
for_each_sg(test_dev.sg->sgl, sg, test_dev.sg->nents, i) {
printk("<%s: %d>addr = 0x%08llx, len = 0x%x\n",
__FUNCTION__, __LINE__, sg->dma_address, sg->length);
}
dma_buf_unmap_attachment(test_dev.attach, test_dev.sg, DMA_TO_DEVICE);
dma_buf_detach(test_dev.dma_buf, test_dev.attach);
dma_buf_put(test_dev.dma_buf);
/* for cpu access */
dma_buf_vmap(test_dev.dma_buf, &test_dev.map);
printk("<%s: %d>addr = 0x%px, str = %s\n",
__FUNCTION__, __LINE__, test_dev.map.vaddr, (char *)test_dev.map.vaddr);
strcpy((char *)test_dev.map.vaddr, "driverB kernel space!");
dma_buf_vunmap(test_dev.dma_buf, &test_dev.map);
break;
}
return 0;
}
2.3 测试log
/ # /app/test
[ 31.678467] fd[4], buf[0x0000ffffb25b9000], size[0x100000]
[ 31.684938] <test_ioctl: 48>addr = 0x68100000, len = 0x100000
[ 31.688969] <test_ioctl: 57>addr = 0xffff800010ba5000, str = driverA: userspace!
driverA buf = driverA kernel space!
[ 31.701777] fd[4], buf[0x0000ffffb25b9000], size[0x100000]
[ 31.703808] <test_ioctl: 70>addr = 0x68100000, len = 0x100000
[ 31.704973] <test_ioctl: 79>addr = 0xffff800010ca6000, str = driverA kernel space!
driverB buf = driverB kernel space!
/ #
2.4 代码分析
结合上图及测试程序我们分析下dma-buf的使用流程。
步骤1打开
/dev/dma_heap/global_cma@68000000
节点,该节点是kernel提供的dma-heap框架创建的一个节点。步骤2通过
ioctl()
从global_cma
中申请一块1M大小内存(global_cma
是从dts中配置的,以0x68000000为起始地址,大小为128M的内存块),对于test
程序来说,申请内存是以data.fd
的形式体现,也就是buf_fd
。步骤3通过
mmap()
函数映射后,就可以对申请的内存进行使用了。步骤4,5打开
/dev/driverA
这个节点,通过ioctl()
函数将infoa这个结构传递到内核空间。然后继续分析测试log。在
test-drvier.c
中(30 ~ 35行)代码,也是通过dma_buf_vmap()
接口,可以将dma_buf
转化为虚拟地址map.vaddr
,这意味着我们就可以通过cpu来操作这块内存了另外打印出内存地址内容(测试log第4行),可以看出就是我们在用户空间往这个地址写的内容
driverA: userspace!
最后把内存内容修改,并返回到用户空间也可以看到内存空间已经被修改(测试log第5行)
首先通过
buf_fd
可以获取dma_buf
结构,然后通过dma_buf_attach()
和dma_buf_map_attachment()
,函数将dma_buf
转化为test_dev.sg
结构test_dev.sg
结构就可以直接给到dma来使用了,另外打印test_dev.sg
结构中的地址可以看出,物理地址是以0x68100000
为基址,大小为1M的内存块,正好落在global_cma
区域(测试log第3行打印)。在
test-driver.c
中(15 ~ 37行)分别测试了dma和cpu两种内存访问方式dma访问内存方式(由于使用qemu环境进行实验,实际没有dma只是从地址打印来看)
cpu访问内存方式
步骤5, 6打开
/dev/driverB
这个节点,通过ioctl()
函数将infob这个结构传递到内核空间。然后继续分析测试log。该部分代码为
test-driver.c
中40 ~ 61行代码,也包含dma和cpu两种内存访问方式主要看下测试log第8行,打印内存内容为
driverA kernel space!
,也就是driverA
中写入的内容,这说明同一块内存可以在不同的驱动之间进行共享
2.5 示例总结
分析完上面的示例程序,简单总结下
首先通过
/dev/dma_heap/global_cma@68000000
节点来从global_cma
来申请内存,该内存对用户空间来说是以fd
的形式体现,经过mmap()
函数映射后,我们就可以对这块内存进行操作了。将
fd
传递到内核空间后,驱动程序可以通过fd
获取其所绑定的dma_buf
结构,通过dma_buf
结构,我们可以得到sg
结构用于dma对内存访问,得到map.vaddr
结构用于cpu对内存访问。另外buf可以在不同驱动之间进行共享,对用户空间buf体现为
fd
,对kernel空间buf体现为dma_buf
结构,而kernel提供的dma-buf
框架用于维护两者的关系从而实现不同驱动之间的buf共享。
2.6 示例扩展
上面的示例程序中只有一个buf,可以进一步扩展为多个buf
test
程序把待处理的数据通过ioctl()
接口put到driverA
的链表上,当driverA
处理完后,可以把处理好的的数据放到另一个链表上,然后通知上层来取(比如通过poll/select机制来通知上层)。上层
test
层拿到driverA
处理的数据又可以通过ioctl()
接口put到driverB
的链表上,当driverB
处理完后,又可以把处理好的数据放到另一个链表上,然后通知上层来取。当然可以有很多这样的驱动程序,前一级的输出结果是下一级的输入,这样一级一级处理,而无须数据在用户空间与内核空间不停的拷贝。
3.dma-buf框架
前面也提到了dma-buf框架是用来管理fd与dma_buf之前关系的,接下来我们介绍下dma-buf框架是怎么实现的。
3.1 exporter
和importer
在dma-buf框架中,有两个概念exporter
和importer
,如上图蓝色虚线框为exporter
,紫色虚线框为importer
。
exporter
是buf的管理者,主要作用如下实现对buf的分配管理,比如,buf从那里分配,分配多少;这些是
exporter
实现的,像上面cma-heap
从名字就可以知道,buf是从cma分配的,用户只需要打开/dev/dma_heap/global_cma@68000000
这个节点申请内存就可以,具体内存怎么申请的,用户是无需关心的。实现对buf的操作,比如在用户空间想要操作这块buf需要先map一下,在内核空间相关通过dma或者cpu来访问该buf,或者做cache同步,这些都是由
exporter
来实现的,如图中左侧的cma_heap_buf_ops
结构,就是该实现的一组操作函数。另外为什么需要由exporter
来实现呢?因为exporter
是清楚这些内存从那里申请的,因此当然清楚怎么map,所以由exporter
来实现这些操作是再合理不过的。importer
是buf的使用者,对于buf的使用者来说就很简单了,只需要拿到buf的fd
,然后通过dma-buf框架提供的接口,就可以对buf的数据进行操作了(前面示例程序也展示了)。
3.2 dma-heap框架
正常来说exporter
和importer
都需要我们自己实现的。比如在嵌入式系统中(有图像处理的场景中),一般需要划分出一段内存区域,用于图像数据的处理,对于该段内存的管理可以使用kernel现有的机制比如cma、保留内存等,当然我们也可以实现一个exporter
而将内存管理起来;而对于importer
往往就是内存的一使用者,一般来说就是我们自己写的驱动程序,比如上面的driverA,driverB测试程序。
kernel提供的dma-heap的框架,就是一个exporter
,其实也非常简单,主要包含下面三个源文件。
drivers/dma-buf/dma-heap.c # 实现dma-heap的框架
drivers/dma-buf/heaps/cma_heap.c # 管理从cma申请的内存
drivers/dma-buf/heaps/system_heap.c # 管理从system申请的内存
dam-heap.c
是一个抽象的接口层,为具体的heap(cma/system)提供注册机制,创建字符设备节点cam_heap.c
用于管理从cma
申请的内存system_heap.c
用于管理从system
申请的内存
dma-heap.c
/* drivers/dma-buf/dma-heap.c */
struct dma_heap *dma_heap_add(const struct dma_heap_export_info *exp_info)
{
...
heap = kzalloc(sizeof(*heap), GFP_KERNEL);
if (!heap)
return ERR_PTR(-ENOMEM);
heap->name = exp_info->name;
heap->ops = exp_info->ops;
heap->priv = exp_info->priv;
/* Find unused minor number */
ret = xa_alloc(&dma_heap_minors, &minor, heap, XA_LIMIT(0, NUM_HEAP_MINORS - 1), GFP_KERNEL);
if (ret < 0) {
pr_err("dma_heap: Unable to get minor number for heap\n");
err_ret = ERR_PTR(ret);
goto err0;
}
/* Create device */
heap->heap_devt = MKDEV(MAJOR(dma_heap_devt), minor);
cdev_init(&heap->heap_cdev, &dma_heap_fops);
ret = cdev_add(&heap->heap_cdev, heap->heap_devt, 1);
dev_ret = device_create(dma_heap_class, NULL, heap->heap_devt, NULL, heap->name);
...
return heap;
}
static long dma_heap_ioctl(struct file *file, unsigned int ucmd,
unsigned long arg)
{
...
switch (kcmd) {
case DMA_HEAP_IOCTL_ALLOC:
ret = dma_heap_ioctl_allocate(file, kdata);
break;
default:
ret = -ENOTTY;
goto err;
}
...
}
dma-heap.c
中提供的两个主要函数dma_heap_add()
和dma_heap_ioctl()
,像cma_heap
、systemp_heap
会通过dma_heap_add()
函数注册到dma-heap
框架来,dma-heap
会分别为他们创建字符设备节点供用户空间使用,并且提供一个通用的dma_heap_fops
(24行代码)。通用的dma_heap_fops
中也简单的提供了一个ioctl
函数,并且只有一个命令DMA_HEAP_IOCTL_ALLOC
(如上dma_heap_ioctl()
函数),当然dam-heap
具体是不清楚对应的heap就从那里申请内存,怎么申请的,它只是通过回调函数走到对应的heap中(简单粗暴)。
cma_heap.c
/* drivers/dma-buf/heaps/cma_heap.c */
static const struct dma_buf_ops cma_heap_buf_ops = {
.attach = cma_heap_attach,
.detach = cma_heap_detach,
.map_dma_buf = cma_heap_map_dma_buf,
.unmap_dma_buf = cma_heap_unmap_dma_buf,
.begin_cpu_access = cma_heap_dma_buf_begin_cpu_access,
.end_cpu_access = cma_heap_dma_buf_end_cpu_access,
.mmap = cma_heap_mmap,
.vmap = cma_heap_vmap,
.vunmap = cma_heap_vunmap,
.release = cma_heap_dma_buf_release,
};
static struct dma_buf *cma_heap_allocate(struct dma_heap *heap,
unsigned long len,
unsigned long fd_flags,
unsigned long heap_flags)
{
...
cma_pages = cma_alloc(cma_heap->cma, pagecount, align, false);
if (!cma_pages)
goto free_buffer;
....
/* create the dmabuf */
exp_info.ops = &cma_heap_buf_ops;
exp_info.size = buffer->len;
exp_info.flags = fd_flags;
exp_info.priv = buffer;
dmabuf = dma_buf_export(&exp_info);
if (IS_ERR(dmabuf)) {
ret = PTR_ERR(dmabuf);
goto free_pages;
}
return dmabuf;
...
}
书接上文,当用户想要申请一块内存时,通过dma-heap
的中转走到cma_heap.c
的cma_heap_allocate()
函数中,可以看到申请是从cma
申请的(第21行);关键看27 ~ 31行代码,通过dma_buf_export()
函数就把该内存放到dma-buf
框架管理起来了。
另外需要关注cma_heap_buf_ops
操作函数集(第27行),对于该组函数从名字来看是不是很熟悉,当使用dma-buf
框架提供的map函数时,dma-buf
也不知道对应的内存该怎样map,最清楚的还是exporter
本身,所以定义了一组回调函数,由exporter
来自己来实现(简单粗暴)。
小结
大家可能都知道android
的ion
,其实它和这里的dma-heap
是处于相同地位的,只不过ion
的实现更复杂。它除了可以在用户空间申请内存外,内核空间也提供了相应的接口,另外它还有一套自己的buf管理机制以及对不同类型buf管理的支持,但底层都是基于dma-buf
来实现的。
3.3 dma-buf
框架
前面说了这么多,终于来到了dma-buf
框架,但实际dma-buf
框架也是相当简单的,主要是学习它这种解决问题的思路。
dma_buf_export()
函数
/* driver/dma-buf/dma-buf.c */
struct dma_buf *dma_buf_export(const struct dma_buf_export_info *exp_info)
{
...
dmabuf = kzalloc(alloc_size, GFP_KERNEL);
if (!dmabuf) {
ret = -ENOMEM;
goto err_module;
}
dmabuf->priv = exp_info->priv;
dmabuf->ops = exp_info->ops;
dmabuf->size = exp_info->size;
dmabuf->exp_name = exp_info->exp_name;
dmabuf->owner = exp_info->owner;
spin_lock_init(&dmabuf->name_lock);
init_waitqueue_head(&dmabuf->poll);
dmabuf->cb_excl.poll = dmabuf->cb_shared.poll = &dmabuf->poll;
dmabuf->cb_excl.active = dmabuf->cb_shared.active = 0;
if (!resv) {
resv = (struct dma_resv *)&dmabuf[1];
dma_resv_init(resv);
}
dmabuf->resv = resv;
file = dma_buf_getfile(dmabuf, exp_info->flags);
if (IS_ERR(file)) {
ret = PTR_ERR(file);
goto err_dmabuf;
}
file->f_mode |= FMODE_LSEEK;
dmabuf->file = file;
mutex_lock(&db_list.lock);
list_add(&dmabuf->list_node, &db_list.head);
mutex_unlock(&db_list.lock);
return dmabuf;
}
前面有提到,对用户空间buf体现为fd
,对kernel空间buf体现为dma_buf
结构,dma-buf
框架用于维护两者的关系,从上面代码可以看到,buf信息首先被包装到一个dma_buf
结构中(第12 ~ 26行),然后通过dma_buf_getfile()
函数将buf与fd绑定(第28 ~ 35行),最后将dma_buf
结构放到一个链表维护起来(38行),是不是相当的简单。
dma_buf_getfile()
函数
/* driver/dma-buf/dma-buf.c */
static const struct file_operations dma_buf_fops = {
.release = dma_buf_file_release,
.mmap = dma_buf_mmap_internal,
.llseek = dma_buf_llseek,
.poll = dma_buf_poll,
.unlocked_ioctl = dma_buf_ioctl,
.compat_ioctl = compat_ptr_ioctl,
.show_fdinfo = dma_buf_show_fdinfo,
};
static struct file *dma_buf_getfile(struct dma_buf *dmabuf, int flags)
{
struct inode *inode = alloc_anon_inode(dma_buf_mnt->mnt_sb);
inode->i_size = dmabuf->size;
inode_set_bytes(inode, dmabuf->size);
file = alloc_file_pseudo(inode, dma_buf_mnt, "dmabuf", flags, &dma_buf_fops);
if (IS_ERR(file))
goto err_alloc_file;
file->f_flags = flags & (O_ACCMODE | O_NONBLOCK);
file->private_data = dmabuf;
file->f_path.dentry->d_fsdata = dmabuf;
return file;
}
再来看一眼dma_buf_getfile()
函数,会通过系统申请一个file
结构,并为该file
绑定一组操作函数集dma_buf_fops
,还记得前面test
测试程序中,有一步map操作吧,该操作函数集也实现了mmap()
函数,我们可以想象mmap()
的实现方式,对于dma-buf
框架来说,它是不清楚要怎么对内存map的,所以需要exporter
自己来实现,如下为dma_buf_mmap_internal()
函数实现。
/* driver/dma-buf/dma-buf.c */
static int dma_buf_mmap_internal(struct file *file, struct vm_area_struct *vma)
{
struct dma_buf *dmabuf;
dmabuf = file->private_data;
/* check if buffer supports mmap */
if (!dmabuf->ops->mmap)
return -EINVAL;
return dmabuf->ops->mmap(dmabuf, vma);
}
dma-buf
框架直接通过回调函数调到exporter
中(第12行),也这印证了我们前面的猜想(虽然事先看过代码😆) ,这里的mmap()
函数是提供给用户空间使用的,像内核空间的dma_buf_attach()
、dma_buf_map_attachment()
和dma_buf_vmap()
等函数,也都是这种回调的实现方式。
小结
经过前面的分析,dma-buf
框架还是非常简单的,每一个buf都被包装成一个dma_buf
结构,然后每一个dma_buf
结构又会绑定一个file
结构,最后将所有的dma_buf
放到链表上维护,这样在内核空间通过遍历该链表就可以由fd
到找对应的dma_buf
结构。另外每一个file
结构还会绑定一组通用的函数操作集,然后通过回调函数的方式,调到对应的exporter
中,这是给用户空间提供的接口;内核空间的dma_buf_xxx()
接口也都采用了正常的实现方式。
参考
https://blog.csdn.net/hexiaolong2009/article/details/102596744
https://www.cnblogs.com/yaongtime/p/14594567.html
https://www.kernel.org/doc/html/latest/driver-api/dma-buf.html#
如果你觉得你现在走得辛苦,那就证明你在走上坡路。🔥